/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

#include <benchmark/benchmark.h>
#include <cstdint>
#include <string>
#include <vector>

#include "bench.pb.h"
#include "fory/serialization/context.h"
#include "fory/serialization/fory.h"
#include "fory/serialization/struct_serializer.h"

// ============================================================================
// Fory struct definitions (must match proto messages)
// ============================================================================

struct NumericStruct {
  int32_t f1;
  int32_t f2;
  int32_t f3;
  int32_t f4;
  int32_t f5;
  int32_t f6;
  int32_t f7;
  int32_t f8;

  bool operator==(const NumericStruct &other) const {
    return f1 == other.f1 && f2 == other.f2 && f3 == other.f3 &&
           f4 == other.f4 && f5 == other.f5 && f6 == other.f6 &&
           f7 == other.f7 && f8 == other.f8;
  }
};
FORY_STRUCT(NumericStruct, f1, f2, f3, f4, f5, f6, f7, f8);

struct Sample {
  int32_t int_value;
  int64_t long_value;
  float float_value;
  double double_value;
  int32_t short_value;
  int32_t char_value;
  bool boolean_value;
  int32_t int_value_boxed;
  int64_t long_value_boxed;
  float float_value_boxed;
  double double_value_boxed;
  int32_t short_value_boxed;
  int32_t char_value_boxed;
  bool boolean_value_boxed;
  std::vector<int32_t> int_array;
  std::vector<int64_t> long_array;
  std::vector<float> float_array;
  std::vector<double> double_array;
  std::vector<int32_t> short_array;
  std::vector<int32_t> char_array;
  std::vector<bool> boolean_array;
  std::string string;

  bool operator==(const Sample &other) const {
    return int_value == other.int_value && long_value == other.long_value &&
           float_value == other.float_value &&
           double_value == other.double_value &&
           short_value == other.short_value && char_value == other.char_value &&
           boolean_value == other.boolean_value &&
           int_value_boxed == other.int_value_boxed &&
           long_value_boxed == other.long_value_boxed &&
           float_value_boxed == other.float_value_boxed &&
           double_value_boxed == other.double_value_boxed &&
           short_value_boxed == other.short_value_boxed &&
           char_value_boxed == other.char_value_boxed &&
           boolean_value_boxed == other.boolean_value_boxed &&
           int_array == other.int_array && long_array == other.long_array &&
           float_array == other.float_array &&
           double_array == other.double_array &&
           short_array == other.short_array && char_array == other.char_array &&
           boolean_array == other.boolean_array && string == other.string;
  }
};
FORY_STRUCT(Sample, int_value, long_value, float_value, double_value,
            short_value, char_value, boolean_value, int_value_boxed,
            long_value_boxed, float_value_boxed, double_value_boxed,
            short_value_boxed, char_value_boxed, boolean_value_boxed, int_array,
            long_array, float_array, double_array, short_array, char_array,
            boolean_array, string);

// Enums for MediaContent benchmark
enum class Player : int32_t { JAVA = 0, FLASH = 1 };

enum class Size : int32_t { SMALL = 0, LARGE = 1 };

struct Media {
  std::string uri;
  std::string title; // Can be empty (null equivalent)
  int32_t width;
  int32_t height;
  std::string format;
  int64_t duration;
  int64_t size;
  int32_t bitrate;
  bool has_bitrate;
  std::vector<std::string> persons;
  Player player;
  std::string copyright;

  bool operator==(const Media &other) const {
    return uri == other.uri && title == other.title && width == other.width &&
           height == other.height && format == other.format &&
           duration == other.duration && size == other.size &&
           bitrate == other.bitrate && has_bitrate == other.has_bitrate &&
           persons == other.persons && player == other.player &&
           copyright == other.copyright;
  }
};
FORY_STRUCT(Media, uri, title, width, height, format, duration, size, bitrate,
            has_bitrate, persons, player, copyright);

struct Image {
  std::string uri;
  std::string title; // Can be empty (null equivalent)
  int32_t width;
  int32_t height;
  Size size;

  bool operator==(const Image &other) const {
    return uri == other.uri && title == other.title && width == other.width &&
           height == other.height && size == other.size;
  }
};
FORY_STRUCT(Image, uri, title, width, height, size);

struct MediaContent {
  Media media;
  std::vector<Image> images;

  bool operator==(const MediaContent &other) const {
    return media == other.media && images == other.images;
  }
};
FORY_STRUCT(MediaContent, media, images);

// ============================================================================
// Test data creation
// ============================================================================

NumericStruct CreateNumericStruct() {
  // Use mixed positive/negative int32 values for realistic benchmark
  return NumericStruct{
      -12345,     // f1: negative
      987654321,  // f2: large positive
      -31415,     // f3: negative
      27182818,   // f4: positive
      -32000,     // f5: negative (near int16 min)
      1000000,    // f6: medium positive
      -999999999, // f7: large negative
      42          // f8: small positive
  };
}

// ============================================================================
// Protobuf conversion functions (like Java benchmark's
// buildPBStruct/fromPBObject)
// ============================================================================

/// Convert plain C++ struct to protobuf message (for serialization)
inline protobuf::Struct ToPbStruct(const NumericStruct &obj) {
  protobuf::Struct pb;
  pb.set_f1(obj.f1);
  pb.set_f2(obj.f2);
  pb.set_f3(obj.f3);
  pb.set_f4(obj.f4);
  pb.set_f5(obj.f5);
  pb.set_f6(obj.f6);
  pb.set_f7(obj.f7);
  pb.set_f8(obj.f8);
  return pb;
}

/// Convert protobuf message to plain C++ struct (for deserialization)
inline NumericStruct FromPbStruct(const protobuf::Struct &pb) {
  NumericStruct obj;
  obj.f1 = pb.f1();
  obj.f2 = pb.f2();
  obj.f3 = pb.f3();
  obj.f4 = pb.f4();
  obj.f5 = pb.f5();
  obj.f6 = pb.f6();
  obj.f7 = pb.f7();
  obj.f8 = pb.f8();
  return obj;
}

protobuf::Struct CreateProtoStruct() {
  return ToPbStruct(CreateNumericStruct());
}

Sample CreateSample() {
  // Consistent with Java Sample.populate() for fair cross-language comparison
  Sample sample;
  sample.int_value = 123;
  sample.long_value = 1230000LL;
  sample.float_value = 12.345f;
  sample.double_value = 1.234567;
  sample.short_value = 12345;
  sample.char_value = '!'; // 33
  sample.boolean_value = true;

  sample.int_value_boxed = 321;
  sample.long_value_boxed = 3210000LL;
  sample.float_value_boxed = 54.321f;
  sample.double_value_boxed = 7.654321;
  sample.short_value_boxed = 32100;
  sample.char_value_boxed = '$'; // 36
  sample.boolean_value_boxed = false;

  // Arrays with mixed positive/negative values (same as Java)
  sample.int_array = {-1234, -123, -12, -1, 0, 1, 12, 123, 1234};
  sample.long_array = {-123400, -12300, -1200, -100,  0,
                       100,     1200,   12300, 123400};
  sample.float_array = {-12.34f, -12.3f, -12.0f, -1.0f, 0.0f,
                        1.0f,    12.0f,  12.3f,  12.34f};
  sample.double_array = {-1.234, -1.23, -12.0, -1.0, 0.0,
                         1.0,    12.0,  1.23,  1.234};
  sample.short_array = {-1234, -123, -12, -1, 0, 1, 12, 123, 1234};
  sample.char_array = {'a', 's', 'd', 'f', 'A', 'S', 'D', 'F'}; // "asdfASDF"
  sample.boolean_array = {true, false, false, true};

  sample.string = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
  return sample;
}

protobuf::Sample CreateProtoSample() {
  // Consistent with Java Sample.populate() for fair cross-language comparison
  protobuf::Sample sample;
  sample.set_int_value(123);
  sample.set_long_value(1230000LL);
  sample.set_float_value(12.345f);
  sample.set_double_value(1.234567);
  sample.set_short_value(12345);
  sample.set_char_value('!'); // 33
  sample.set_boolean_value(true);

  sample.set_int_value_boxed(321);
  sample.set_long_value_boxed(3210000LL);
  sample.set_float_value_boxed(54.321f);
  sample.set_double_value_boxed(7.654321);
  sample.set_short_value_boxed(32100);
  sample.set_char_value_boxed('$'); // 36
  sample.set_boolean_value_boxed(false);

  // Arrays with mixed positive/negative values (same as Java)
  for (int v : {-1234, -123, -12, -1, 0, 1, 12, 123, 1234}) {
    sample.add_int_array(v);
  }
  for (int64_t v : {-123400LL, -12300LL, -1200LL, -100LL, 0LL, 100LL, 1200LL,
                    12300LL, 123400LL}) {
    sample.add_long_array(v);
  }
  for (float v :
       {-12.34f, -12.3f, -12.0f, -1.0f, 0.0f, 1.0f, 12.0f, 12.3f, 12.34f}) {
    sample.add_float_array(v);
  }
  for (double v : {-1.234, -1.23, -12.0, -1.0, 0.0, 1.0, 12.0, 1.23, 1.234}) {
    sample.add_double_array(v);
  }
  for (int v : {-1234, -123, -12, -1, 0, 1, 12, 123, 1234}) {
    sample.add_short_array(v);
  }
  for (int v : {'a', 's', 'd', 'f', 'A', 'S', 'D', 'F'}) { // "asdfASDF"
    sample.add_char_array(v);
  }
  for (bool v : {true, false, false, true}) {
    sample.add_boolean_array(v);
  }
  sample.set_string("ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789");
  return sample;
}

MediaContent CreateMediaContent() {
  // Matches Java MediaContent.populate(false) - no circular reference
  MediaContent content;

  // Media fields matching Java populate()
  content.media.uri = "http://javaone.com/keynote.ogg";
  content.media.title = ""; // null in Java
  content.media.width = 641;
  content.media.height = 481;
  content.media.format = u8"video/theora\u1234"; // UTF-8 encoded unicode
  content.media.duration = 18000001;
  content.media.size = 58982401;
  content.media.bitrate = 0;
  content.media.has_bitrate = false;
  content.media.persons = {"Bill Gates, Jr.", "Steven Jobs"};
  content.media.player = Player::FLASH;
  content.media.copyright = "Copyright (c) 2009, Scooby Dooby Doo";

  // Images matching Java populate(false) - no circular reference
  content.images = {
      Image{"http://javaone.com/keynote_huge.jpg", u8"Javaone Keynote\u1234",
            32000, 24000, Size::LARGE},
      Image{"http://javaone.com/keynote_large.jpg", "", 1024, 768, Size::LARGE},
      Image{"http://javaone.com/keynote_small.jpg", "", 320, 240, Size::SMALL}};

  return content;
}

/// Convert Image to protobuf
inline protobuf::Image ToPbImage(const Image &img) {
  protobuf::Image pb;
  pb.set_uri(img.uri);
  if (!img.title.empty()) {
    pb.set_title(img.title);
  }
  pb.set_width(img.width);
  pb.set_height(img.height);
  pb.set_size(static_cast<protobuf::Size>(img.size));
  return pb;
}

/// Convert Media to protobuf
inline protobuf::Media ToPbMedia(const Media &m) {
  protobuf::Media pb;
  pb.set_uri(m.uri);
  if (!m.title.empty()) {
    pb.set_title(m.title);
  }
  pb.set_width(m.width);
  pb.set_height(m.height);
  pb.set_format(m.format);
  pb.set_duration(m.duration);
  pb.set_size(m.size);
  pb.set_bitrate(m.bitrate);
  pb.set_has_bitrate(m.has_bitrate);
  for (const auto &person : m.persons) {
    pb.add_persons(person);
  }
  pb.set_player(static_cast<protobuf::Player>(m.player));
  pb.set_copyright(m.copyright);
  return pb;
}

/// Convert MediaContent to protobuf
inline protobuf::MediaContent ToPbMediaContent(const MediaContent &mc) {
  protobuf::MediaContent pb;
  *pb.mutable_media() = ToPbMedia(mc.media);
  for (const auto &img : mc.images) {
    *pb.add_images() = ToPbImage(img);
  }
  return pb;
}

/// Convert protobuf to Image
inline Image FromPbImage(const protobuf::Image &pb) {
  Image img;
  img.uri = pb.uri();
  img.title = pb.has_title() ? pb.title() : "";
  img.width = pb.width();
  img.height = pb.height();
  img.size = static_cast<Size>(pb.size());
  return img;
}

/// Convert protobuf to Media
inline Media FromPbMedia(const protobuf::Media &pb) {
  Media m;
  m.uri = pb.uri();
  m.title = pb.has_title() ? pb.title() : "";
  m.width = pb.width();
  m.height = pb.height();
  m.format = pb.format();
  m.duration = pb.duration();
  m.size = pb.size();
  m.bitrate = pb.bitrate();
  m.has_bitrate = pb.has_bitrate();
  for (const auto &person : pb.persons()) {
    m.persons.push_back(person);
  }
  m.player = static_cast<Player>(pb.player());
  m.copyright = pb.copyright();
  return m;
}

/// Convert protobuf to MediaContent
inline MediaContent FromPbMediaContent(const protobuf::MediaContent &pb) {
  MediaContent mc;
  mc.media = FromPbMedia(pb.media());
  for (const auto &img : pb.images()) {
    mc.images.push_back(FromPbImage(img));
  }
  return mc;
}

protobuf::MediaContent CreateProtoMediaContent() {
  return ToPbMediaContent(CreateMediaContent());
}

// ============================================================================
// Helper to configure Fory instance
// ============================================================================

void RegisterForyTypes(fory::serialization::Fory &fory) {
  fory.register_struct<NumericStruct>(1);
  fory.register_struct<Sample>(2);
  fory.register_struct<Media>(3);
  fory.register_struct<Image>(4);
  fory.register_struct<MediaContent>(5);
}

// ============================================================================
// Struct benchmarks (simple object with 8 int32 fields)
// ============================================================================

static void BM_Fory_Struct_Serialize(benchmark::State &state) {
  auto fory = fory::serialization::Fory::builder()
                  .xlang(true)
                  .track_ref(false)
                  .check_struct_version(false)
                  .build();
  RegisterForyTypes(fory);
  NumericStruct obj = CreateNumericStruct();

  // Reuse internal buffer
  fory::Buffer buffer;
  buffer.Reserve(64);

  for (auto _ : state) {
    buffer.WriterIndex(0);
    fory.serialize_to(obj, buffer);
    benchmark::DoNotOptimize(buffer.data());
  }
}
BENCHMARK(BM_Fory_Struct_Serialize);

// Fair comparison: convert plain C++ struct to protobuf, then serialize
// (Same pattern as Java benchmark's buildPBStruct().toByteArray())
static void BM_Protobuf_Struct_Serialize(benchmark::State &state) {
  NumericStruct obj = CreateNumericStruct();
  protobuf::Struct pb = ToPbStruct(obj);
  std::vector<uint8_t> output;
  output.resize(pb.ByteSizeLong());

  for (auto _ : state) {
    pb = ToPbStruct(obj);
    pb.SerializeToArray(output.data(), static_cast<int>(output.size()));
    benchmark::DoNotOptimize(output);
  }
}
BENCHMARK(BM_Protobuf_Struct_Serialize);

static void BM_Fory_Struct_Deserialize(benchmark::State &state) {
  auto fory = fory::serialization::Fory::builder()
                  .xlang(true)
                  .track_ref(false)
                  .check_struct_version(false)
                  .build();
  RegisterForyTypes(fory);
  NumericStruct obj = CreateNumericStruct();
  auto serialized = fory.serialize(obj);
  if (!serialized.ok()) {
    state.SkipWithError("Serialization failed");
    return;
  }
  auto &bytes = serialized.value();

  // Verify deserialization works first
  auto test_result =
      fory.deserialize<NumericStruct>(bytes.data(), bytes.size());
  if (!test_result.ok()) {
    state.SkipWithError("Deserialization test failed");
    return;
  }

  for (auto _ : state) {
    auto result = fory.deserialize<NumericStruct>(bytes.data(), bytes.size());
    benchmark::DoNotOptimize(result);
  }
}
BENCHMARK(BM_Fory_Struct_Deserialize);

// Fair comparison: deserialize and convert protobuf to plain C++ struct
// (Same pattern as Java benchmark's fromPBObject())
static void BM_Protobuf_Struct_Deserialize(benchmark::State &state) {
  protobuf::Struct obj = CreateProtoStruct();
  std::string serialized;
  obj.SerializeToString(&serialized);

  for (auto _ : state) {
    protobuf::Struct pb_result;
    pb_result.ParseFromString(serialized);
    NumericStruct result = FromPbStruct(pb_result);
    benchmark::DoNotOptimize(result);
  }
}
BENCHMARK(BM_Protobuf_Struct_Deserialize);

// ============================================================================
// Sample benchmarks (complex object with various types and arrays)
// ============================================================================

static void BM_Fory_Sample_Serialize(benchmark::State &state) {
  auto fory = fory::serialization::Fory::builder()
                  .xlang(true)
                  .track_ref(false)
                  .check_struct_version(false)
                  .build();
  RegisterForyTypes(fory);
  Sample obj = CreateSample();

  // Pre-allocate buffer (like Protobuf benchmark does)
  fory::Buffer buffer;
  buffer.Reserve(4096);

  for (auto _ : state) {
    buffer.WriterIndex(0);
    auto result = fory.serialize_to(obj, buffer);
    benchmark::DoNotOptimize(result);
    benchmark::DoNotOptimize(buffer.data());
  }
}
BENCHMARK(BM_Fory_Sample_Serialize);

static void BM_Protobuf_Sample_Serialize(benchmark::State &state) {
  protobuf::Sample obj = CreateProtoSample();
  std::vector<uint8_t> output;
  output.resize(obj.ByteSizeLong());

  for (auto _ : state) {
    obj.SerializeToArray(output.data(), static_cast<int>(output.size()));
    benchmark::DoNotOptimize(output);
  }
}
BENCHMARK(BM_Protobuf_Sample_Serialize);

static void BM_Fory_Sample_Deserialize(benchmark::State &state) {
  auto fory = fory::serialization::Fory::builder()
                  .xlang(true)
                  .track_ref(false)
                  .check_struct_version(false)
                  .build();
  RegisterForyTypes(fory);
  Sample obj = CreateSample();
  auto serialized = fory.serialize(obj);
  if (!serialized.ok()) {
    state.SkipWithError("Serialization failed");
    return;
  }
  auto &bytes = serialized.value();

  // Verify deserialization works first
  auto test_result = fory.deserialize<Sample>(bytes.data(), bytes.size());
  if (!test_result.ok()) {
    state.SkipWithError("Deserialization test failed");
    return;
  }

  for (auto _ : state) {
    auto result = fory.deserialize<Sample>(bytes.data(), bytes.size());
    benchmark::DoNotOptimize(result);
  }
}
BENCHMARK(BM_Fory_Sample_Deserialize);

static void BM_Protobuf_Sample_Deserialize(benchmark::State &state) {
  protobuf::Sample obj = CreateProtoSample();
  std::string serialized;
  obj.SerializeToString(&serialized);

  for (auto _ : state) {
    protobuf::Sample result;
    result.ParseFromString(serialized);
    benchmark::DoNotOptimize(result);
  }
}
BENCHMARK(BM_Protobuf_Sample_Deserialize);

// ============================================================================
// MediaContent benchmarks (nested objects with strings and lists)
// ============================================================================

static void BM_Fory_MediaContent_Serialize(benchmark::State &state) {
  auto fory = fory::serialization::Fory::builder()
                  .xlang(true)
                  .track_ref(false)
                  .check_struct_version(false)
                  .build();
  RegisterForyTypes(fory);
  MediaContent obj = CreateMediaContent();

  // Pre-allocate buffer
  fory::Buffer buffer;
  buffer.Reserve(4096);

  for (auto _ : state) {
    buffer.WriterIndex(0);
    auto result = fory.serialize_to(obj, buffer);
    benchmark::DoNotOptimize(result);
    benchmark::DoNotOptimize(buffer.data());
  }
}
BENCHMARK(BM_Fory_MediaContent_Serialize);

static void BM_Protobuf_MediaContent_Serialize(benchmark::State &state) {
  MediaContent obj = CreateMediaContent();
  protobuf::MediaContent pb = ToPbMediaContent(obj);
  std::vector<uint8_t> output;
  output.resize(pb.ByteSizeLong());

  for (auto _ : state) {
    pb = ToPbMediaContent(obj);
    pb.SerializeToArray(output.data(), static_cast<int>(output.size()));
    benchmark::DoNotOptimize(output);
  }
}
BENCHMARK(BM_Protobuf_MediaContent_Serialize);

static void BM_Fory_MediaContent_Deserialize(benchmark::State &state) {
  auto fory = fory::serialization::Fory::builder()
                  .xlang(true)
                  .track_ref(false)
                  .check_struct_version(false)
                  .build();
  RegisterForyTypes(fory);
  MediaContent obj = CreateMediaContent();
  auto serialized = fory.serialize(obj);
  if (!serialized.ok()) {
    state.SkipWithError("Serialization failed");
    return;
  }
  auto &bytes = serialized.value();

  // Verify deserialization works first
  auto test_result = fory.deserialize<MediaContent>(bytes.data(), bytes.size());
  if (!test_result.ok()) {
    state.SkipWithError("Deserialization test failed");
    return;
  }

  for (auto _ : state) {
    auto result = fory.deserialize<MediaContent>(bytes.data(), bytes.size());
    benchmark::DoNotOptimize(result);
  }
}
BENCHMARK(BM_Fory_MediaContent_Deserialize);

static void BM_Protobuf_MediaContent_Deserialize(benchmark::State &state) {
  protobuf::MediaContent obj = CreateProtoMediaContent();
  std::string serialized;
  obj.SerializeToString(&serialized);

  for (auto _ : state) {
    protobuf::MediaContent pb_result;
    pb_result.ParseFromString(serialized);
    MediaContent result = FromPbMediaContent(pb_result);
    benchmark::DoNotOptimize(result);
  }
}
BENCHMARK(BM_Protobuf_MediaContent_Deserialize);

// ============================================================================
// Serialized size comparison (printed once at the end)
// ============================================================================

static void BM_PrintSerializedSizes(benchmark::State &state) {
  // Fory
  auto fory = fory::serialization::Fory::builder()
                  .xlang(true)
                  .track_ref(false)
                  .check_struct_version(false)
                  .build();
  RegisterForyTypes(fory);
  NumericStruct fory_struct = CreateNumericStruct();
  Sample fory_sample = CreateSample();
  MediaContent fory_media = CreateMediaContent();
  auto fory_struct_bytes = fory.serialize(fory_struct).value();
  auto fory_sample_bytes = fory.serialize(fory_sample).value();
  auto fory_media_bytes = fory.serialize(fory_media).value();

  // Protobuf
  protobuf::Struct proto_struct = CreateProtoStruct();
  protobuf::Sample proto_sample = CreateProtoSample();
  protobuf::MediaContent proto_media = CreateProtoMediaContent();
  std::string proto_struct_bytes, proto_sample_bytes, proto_media_bytes;
  proto_struct.SerializeToString(&proto_struct_bytes);
  proto_sample.SerializeToString(&proto_sample_bytes);
  proto_media.SerializeToString(&proto_media_bytes);

  for (auto _ : state) {
    // Just run once to print sizes
  }

  state.counters["fory_struct_size"] = fory_struct_bytes.size();
  state.counters["proto_struct_size"] = proto_struct_bytes.size();
  state.counters["fory_sample_size"] = fory_sample_bytes.size();
  state.counters["proto_sample_size"] = proto_sample_bytes.size();
  state.counters["fory_media_size"] = fory_media_bytes.size();
  state.counters["proto_media_size"] = proto_media_bytes.size();
}
BENCHMARK(BM_PrintSerializedSizes)->Iterations(1);
